/*
 * Copyright (c) 2009-2013 EvTech Project
 * All rights reserved.
 * 
 * This file is part of Symbol Matching.
 *
 * Symbol Matching is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or 
 * (at your option) any later version.
 *
 * Symbol Matching is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Symbol Matching.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package fi.honkalampisaatio.reaktiopeli.game;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Random;

import javax.sound.sampled.Clip;

import fi.honkalampisaatio.game.game.LanguageResource;
import fi.honkalampisaatio.game.xml.SettingsReader;
import fi.honkalampisaatio.game.xml.SettingsWriter;
import fi.honkalampisaatio.reaktiopeli.xml.GameData;
import fi.honkalampisaatio.reaktiopeli.xml.GameDifficulty;
import fi.honkalampisaatio.reaktiopeli.xml.GameDifficulty.DIFFICULTY;
import fi.honkalampisaatio.reaktiopeli.xml.GameTime;
import fi.honkalampisaatio.reaktiopeli.xml.HighScore;
import fi.honkalampisaatio.reaktiopeli.xml.PlayerData;
import fi.honkalampisaatio.reaktiopeli.xml.PlayerHighscores;
import fi.honkalampisaatio.reaktiopeli.xml.ReactionGameSettings;
import fi.honkalampisaatio.reaktiopeli.setup.SetupData;
import fi.honkalampisaatio.reaktiopeli.setup.SetupEvent;
import fi.honkalampisaatio.reaktiopeli.setup.SetupEventListener;
import fi.honkalampisaatio.reaktiopeli.target.TargetEvent;
import fi.honkalampisaatio.reaktiopeli.target.TargetEventListener;


public class GameRunner implements TargetEventListener, SetupEventListener{
	private static final int MINTIME = 500;
	private static final float SPEED_UP = 0.9f;
	
	private enum GAMESTATE {SETUP, NEW, WAIT_TARGETS, MEMORIZE, WAIT, SHOW, END, HIGHSCORES};
	private GAMESTATE gameState;
	private GamePanel gp;
	private boolean isRunning;
	private boolean bProcessingEvent;
	private String instructions;
	/**
	 * Random values
	 */
	private Random rnd = new Random();
	private long lastTime;
	private long currentShowTime;
	private int waitTime;
	private long gameTime;

	private SetupData setupData;
	private LanguageResource dictionary;
	
	private String playerDataFile;
	private String gamepath;
	
	private Hashtable<Player, PlayerHighscores> highScores;

	public GameRunner(GamePanel game) {
		this.gp = game;
		this.gameState = GAMESTATE.SETUP;
		bProcessingEvent = false;
		setupData = new SetupData();
		isRunning = false;
		dictionary = LanguageResource.getInstance();
		highScores = new Hashtable<Player, PlayerHighscores>();
	}


	public void shutdown() {
		this.gameOver();
		System.out.println("Game is correctly shut down");
		System.exit(0);
		
	}

	public boolean isRunning() {
		return this.isRunning;
	}


	public void gameOver() {
		// koska tätä voidaan kutsua muualtakin, tsekataan ensin, että
		// onko peli vielä käynnissä. Jos on, pakotetaan sen loppumaan 
		// muuttamalla running muuttuja falseksi...
		if (this.isRunning) {
			this.isRunning = false;
			return;
		}
		this.gp.gameEnd();
	}

	public void gameStart() {
		// Create a new thread
	    Thread gameThread =  new Thread() {
	    	// Override run() to provide the running behavior of this thread.
	        @Override
	        public void run() {
	        	gameLoop();
	        }};

		// Start the thread. start() calls run(), which in turn calls gameLoop().
		gameThread.start();
	}

	public void setModel(ReactionGameSettings settings, String settingsfile) {
		System.out.println("GameRunner.setModel");
		this.gamepath = settingsfile;
		waitTime = settings.getWaitTime();
		instructions = settings.getInstruction();
		setupData.setGameData(settings);
	}


	private void gameLoop() {
		long startTime, loopStartTime, lastGamerefreshTime;
		int rightAnswer = -1;
		int prevRightAnswer = -1;
		int currentRound = 0;
		int buttonCount = gp.getChoiceCount();


		this.gp.logAllPlayers("GAME STARTED");
		
		
		this.isRunning = buttonCount>0 ? true : false;
		currentShowTime = setupData.getGameDifficulty().getRoundTime();
		startTime = System.currentTimeMillis();
		lastTime = startTime;
		gameTime = 0;
		lastGamerefreshTime = 0;
		loadSetup();
		loadHighScores();
		gp.showInstructionsPanel();
		//gp.showSetup(setupData);
		this.gameState = GAMESTATE.SETUP;
		bProcessingEvent = false;
		while(isRunning) {
			try {
				Thread.sleep(30);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			loopStartTime = System.currentTimeMillis();
			switch(this.gameState) {
				case SETUP:
					break;
				case NEW:
					if(loopStartTime-lastTime>waitTime) {
						Clip sound = null;
						currentRound++;
						gp.clearTargets();
						rightAnswer = rnd.nextInt(buttonCount);
						// Fast hack
						while(prevRightAnswer==rightAnswer && buttonCount>1)
						{
							rightAnswer = rnd.nextInt(buttonCount);
						}
						prevRightAnswer = rightAnswer;
						
						sound = this.gp.showChoices(rightAnswer, instructions);
						//this.gp.showTimeMeter(currentShowTime, currentShowTime);
						this.gp.newRound(currentShowTime);
						this.gp.logAllPlayers("New target " + rightAnswer + " (" + 
								gp.getButtonNameById(rightAnswer) + ")");
						this.gameState = GAMESTATE.SHOW;
						this.gp.playClip(sound);
						lastTime = System.currentTimeMillis();
						this.gp.repaint();
						lastGamerefreshTime = lastTime;
					}
					break;
				case WAIT_TARGETS:
					break;
				case MEMORIZE:
					if(loopStartTime-lastTime>waitTime) {
						this.gp.showWait();
						lastTime = System.currentTimeMillis();
						this.gameState = GAMESTATE.WAIT;
					}
					break;
				case WAIT:
					if(loopStartTime-lastTime>waitTime) {
						lastTime = System.currentTimeMillis();
						this.gameState = GAMESTATE.SHOW;
					}
					break;
				case SHOW:
					if(loopStartTime-lastTime>currentShowTime && currentShowTime!=0 && !bProcessingEvent) {
						//this.gp.hideTimeMeter();
						if(setupData.getGameDifficulty().getRoundTime()>0)
						{
							currentShowTime = (long)(currentShowTime * SPEED_UP);
							currentShowTime = currentShowTime>MINTIME ? currentShowTime : MINTIME;
						}
						lastTime = System.currentTimeMillis();
						if(this.gp.getCountOfSucceededPlayers()<=0){
							System.out.println("playEndClip");
							this.gp.logAllPlayers("Time out");
							this.gp.showFeedback(dictionary.get("GAMEOVER_TIMEOUT_TEXT"), false);
							this.gameState = GAMESTATE.HIGHSCORES;
							this.gp.playEndClip();
							lastTime = System.currentTimeMillis();
						}
						else {
							this.gp.logAllPlayers("Time out");
							this.gp.showFeedback(dictionary.get("NEWROUND_TEXT"), true);
							this.gameState = GAMESTATE.NEW;
							lastTime = System.currentTimeMillis();
						}
					} 
					else {
						gameTime += loopStartTime - lastGamerefreshTime;
						//this.gp.showTimeMeter(currentShowTime-loopStartTime+lastTime, currentShowTime);
						this.gp.refreshGameTime(loopStartTime - lastGamerefreshTime);
						lastGamerefreshTime = loopStartTime;
					}
					break;
				case HIGHSCORES:
					if(loopStartTime-lastTime>waitTime) {
						lastTime = System.currentTimeMillis();
						DIFFICULTY difficulty = setupData.getGameDifficulty().getDifficulty();
						if(difficulty==null)
						{
							difficulty = DIFFICULTY.NORMAL;
						}
						this.gp.showHighScore(difficulty, saveHighScores());
						this.gameState = GAMESTATE.END;
						
					}
					break;
				case END:
					/*
					if(loopStartTime-lastTime>waitTime) {
						lastTime = System.currentTimeMillis();
						this.gp.hideHighScore();
						this.isRunning = false;
						
					}
					*/
					break;
			}
			if(currentRound>setupData.getGameDifficulty().getRounds() &&
					setupData.getGameDifficulty().getRounds()>0 &&
					!(this.gameState == GAMESTATE.HIGHSCORES || this.gameState == GAMESTATE.END)) {
				//this.gp.hideTimeMeter();
				//lastTime = System.currentTimeMillis();
				this.gp.logAllPlayers("All rounds played");
				this.gp.showFeedback(dictionary.get("GAMEOVER_ALLROUNDSPLAYED_TEXT"), false);
				this.gp.playEndClip();
				lastTime = System.currentTimeMillis();
				this.gameState = GAMESTATE.HIGHSCORES;
			}
			/* for CASCATE
			if(currentRound>10 && setupData.getQuitConditions().getTotalRounds()==0
					&& (setupData.isOmitMistakes() || 
					setupData.getQuitConditions().getWrongAnswer() == ANSWER.NONE))
			{
				this.gp.hideTimeMeter();
				//lastTime = System.currentTimeMillis();
				this.gp.logAllPlayers("All rounds played");
				this.gp.showFeedback(dictionary.get("GAMEOVER_ALLROUNDSPLAYED_TEXT"));
				this.gameState = GAMESTATE.END;
			}
			*/
			
		}
		this.gameOver();
	}


	private Hashtable<Player, HighScore> saveHighScores() {
		Hashtable<Player, HighScore> gameHighScores = new Hashtable<Player, HighScore>();
		DIFFICULTY difficulty = setupData.getGameDifficulty().getDifficulty();
		if(difficulty==null)
		{
			difficulty = DIFFICULTY.NORMAL;
		}
		//save highscores for every player
		if(gp.getPlayers()!=null) {
			Iterator<Player> it = gp.getPlayers().iterator();
			while(it.hasNext()) {
				Player player = it.next();
				PlayerHighscores playerHighscores = this.highScores.get(player);
				HighScore currentGameHighScore = null;
				GameTime currentGameTime = new GameTime();
				currentGameTime.setTime(gameTime);
				currentGameTime.setScore(player.getScore());
				for(int i=0; playerHighscores!=null && 
					playerHighscores.getGames()!=null && 
					i<playerHighscores.getGames().size(); i++)
				{
					HighScore hs = playerHighscores.getGames().get(i);
					if(hs!=null && hs.getGamePath()!=null && 
							hs.getGamePath().equalsIgnoreCase(gamepath))
					{
						currentGameHighScore = playerHighscores.getGames().get(i);
						break;
					}
				}
				if(currentGameHighScore==null)
				{
					currentGameHighScore = new HighScore();
					currentGameHighScore.setGamePath(gamepath);
					playerHighscores.getGames().add(currentGameHighScore);
				}
				if(currentGameHighScore.addGameTime(difficulty, currentGameTime)>=0)
				{
					this.gp.logAllPlayers("New highscore for "+player.getPlayerName());
					String highscoreFile = player.getFolder()+"highscores.xml";
					try {
						SettingsWriter.getInstance().writeGameSettings(highscoreFile, playerHighscores);
					} catch (Exception e) {
						System.out.println("highscoreFile: "+highscoreFile);
						e.printStackTrace();
					}
				}
				gameHighScores.put(player, currentGameHighScore);
			}
		}
		return gameHighScores;
	}

	private void loadHighScores() {
		//load highscores for every player
		if(gp.getPlayers()!=null) {
			Iterator<Player> it = gp.getPlayers().iterator();
			while(it.hasNext()) {
				Player player = it.next();
				PlayerHighscores playerHighscores = null;
				String highscoreFile = player.getFolder()+"highscores.xml";
				try {
					playerHighscores = (PlayerHighscores) 
						SettingsReader.getInstance().readSettings(
								highscoreFile, PlayerHighscores.class);
				} catch (Exception e) {
					System.out.println("load highscore failed: "+highscoreFile);
				}
				if(playerHighscores!=null)
				{
					this.highScores.put(player, playerHighscores);
				}
				else
				{
					playerHighscores = new PlayerHighscores();
					this.highScores.put(player, playerHighscores);
				}
				
			}
		}
		
	}


	private void loadSetup() {
		try {
			PlayerData playerData = (PlayerData) SettingsReader.getInstance().readSettings(
					playerDataFile, PlayerData.class);
			if(playerData!=null && playerData.getSavedGames()!=null)
			{
				for(int i=0; i<playerData.getSavedGames().size(); i++)
				{
					GameData gameData = playerData.getSavedGames().get(i);
					if(gameData.getGame().equalsIgnoreCase(gamepath))
					{
						setupData = gameData.getSetupData();
					}
				}
			}
		} catch (Exception e) {
			System.out.println("load setup failed: "+playerDataFile);
		}
		refreshSetupData();
	}


	@Override
	public void targetEventOccurred(TargetEvent e) {		
		bProcessingEvent = true;
		if(e==null) {
			System.out.println("e == null");
			bProcessingEvent = false;
			return;
		}
		if(e.getEventType()==null) { 
			System.out.println("e.getEventType() == null");
			bProcessingEvent = false;
			return;
		}
		if(this.gameState==GAMESTATE.END)
		{
			lastTime = System.currentTimeMillis();
			this.gp.hideHighScore();
			this.isRunning = false;
		}
		else if(e.getEventType()==TargetEvent.TYPE.READY && 
				this.gameState==GAMESTATE.WAIT_TARGETS) {
			System.out.println("targetEventOccurred show");
			lastTime = System.currentTimeMillis();
			this.gameState = GAMESTATE.SHOW;
		}
		else if(e.getEventType()==TargetEvent.TYPE.PRESS && 
				this.gameState==GAMESTATE.SHOW) {
			if(e.getSource() instanceof Player) {
				((Player)e.getSource()).hitTarget(e.getId());
			}
			if (e.getSource() instanceof ChoicePanel) { // If it's the choicepanel we'r using, we must also find the player that uses mouse to press it
				ArrayList <Player>plrs = new ArrayList<Player>();
				plrs = gp.getPlayers();
				if (e.getUser()==TargetEvent.PLAYER.PLAYER_1) // get user tells here who was responsible for pressing the buttons as we cannot use player class for now
					( (Player)plrs.get(0) ).hitTarget(e.getId());
				else
					( (Player)plrs.get(1) ).hitTarget(e.getId());
			}
			if(this.gp.getCountOfSucceededPlayers()>0 && 
					this.gp.getCountOfSucceededPlayers() + this.gp.getCountOfFailedPlayers() == this.gp.getCountOfPlayers()) {
				if(setupData.getGameDifficulty().getRoundTime()>0)
				{
					currentShowTime = (long)(currentShowTime * SPEED_UP);
					currentShowTime = currentShowTime>MINTIME ? currentShowTime : MINTIME;
				}
				this.gp.showFeedback(dictionary.get("NEWROUND_TEXT"), true);
				lastTime = System.currentTimeMillis();
				this.gameState = GAMESTATE.NEW;
			}
			else if(!setupData.isOmitMistakes() && 
					this.gp.getCountOfFailedPlayers() == this.gp.getCountOfPlayers()) {
				this.gp.showFeedback(dictionary.get("GAME_END_TEXT"), false);
				this.gameState = GAMESTATE.HIGHSCORES;
				lastTime = System.currentTimeMillis();
				this.gp.playEndClip();
			}
		}
		bProcessingEvent = false;
		this.gp.repaint();
	}


	@Override
	public void setupEventOccurred(SetupEvent e) {
		if(e==null) {
			System.out.println("e == null");
			return;
		}
		switch(e.getMessage())
		{
		case OK:
			if(e.getData()!=null) {
				setupData = e.getData();
				refreshSetupData();
				saveSetupData();
			}
			gp.hideInstructionsPanel();
			gp.hideSetup();
			this.gameState = GAMESTATE.NEW;
			break;
		case CANCEL:
			gp.hideInstructionsPanel();
			gp.hideSetup();
			this.gameOver();
			break;
		case SETUP:
			gp.hideInstructionsPanel();
			gp.showSetup(setupData);
		break;
		}
		
	}
	
	public void saveSetupData()
	{
		PlayerData playerData = null;
		boolean isNew = true;
		try {
			playerData = (PlayerData) SettingsReader.getInstance().readSettings(
					playerDataFile, PlayerData.class);
		} catch (Exception e) {
			System.out.println("old data file not found: "+playerDataFile);
		}
		if(playerData==null)
		{
			playerData = new PlayerData();
		}
		if(playerData.getSavedGames()==null)
		{
			playerData.setSavedGames(new ArrayList<GameData>());
		}
		for(int i=0; i<playerData.getSavedGames().size(); i++)
		{
			GameData gameData = playerData.getSavedGames().get(i);
			if(gameData.getGame().equalsIgnoreCase(gamepath))
			{
				gameData.setSetupData(setupData);
				isNew = false;
			}
		}
		if(isNew)
		{
			GameData gameData = new GameData();
			gameData.setGame(gamepath);
			gameData.setSetupData(setupData);
			playerData.getSavedGames().add(gameData);
		}
		try {
			SettingsWriter.getInstance().writeGameSettings(playerDataFile, playerData);
		} catch (Exception e) {
			System.out.println("setupDataFile: "+playerDataFile);
			e.printStackTrace();
		}
	}


	public void setSaveFile(String playerDataFile) {
		this.playerDataFile = playerDataFile;
	}
	
	private void refreshSetupData()
	{
		if(setupData==null)
			return;
		gp.setShowScore(setupData.isShowScore());
		gp.setShowTime(setupData.isShowTime());
		if(setupData.getGameDifficulty()==null)
		{
			setupData.setGameDifficulty(new GameDifficulty());
		}
		currentShowTime = setupData.getGameDifficulty().getRoundTime();
		gp.refreshSetupData(setupData);
	}

}
